Skip to content

RFC: rework output configuration options and their consumption - #4966

Open
behackl wants to merge 14 commits into
ManimCommunity:mainfrom
behackl:refactor/output-session-config
Open

RFC: rework output configuration options and their consumption#4966
behackl wants to merge 14 commits into
ManimCommunity:mainfrom
behackl:refactor/output-session-config

Conversation

@behackl

@behackl behackl commented Aug 26, 2026

Copy link
Copy Markdown
Member

Summary

This RFC normalizes render output and presentation configuration into one immutable session specification, resolved once when a Scene is initialized.

At present, output intent is spread across format, write_to_movie, save_last_frame, save_pngs, save_as_gif, transparency, dry_run, and renderer-specific preview behavior. Those values can conflict, depend on CLI/config parsing order, and are reread or mutated while rendering. In particular, OpenGL and Cairo currently interpret -p differently, and SceneFileWriter decides what to produce by repeatedly consulting mutable global configuration.

The proposed model has:

  • one canonical format value for the primary artifact;
  • a frozen OutputSpec for resolved artifact intent;
  • a frozen PresentationSpec for post-render and live presentation requests;
  • a RenderSessionSpec combining both and preserving dry-run execution intent;
  • explicit renderer capabilities, currently only live_preview;
  • one resolution point in Scene, before renderer initialization; and
  • an explicitly supplied OutputSpec for SceneFileWriter, with no fallback to global output configuration.

This is intended as a foundation for the subsequent Manager/timeline and audio-aware file-writer work, without attempting that larger decomposition here.

User-facing output model

--format selects one primary artifact:

Value Meaning
auto Default. Resolves to MP4 for opaque output and MOV for transparent output. With live preview, resolves to no file unless a concrete format is requested.
none Evaluate the scene without producing a primary media artifact.
mp4, mov, webm, gif Produce the selected time-based artifact.
png Fast-forward animations and write only the final evaluated scene state. Equivalent to -s / --save_last_frame.
png-sequence Evaluate the full frame progression and write every frame as a numbered PNG.

dry_run remains a separate execution request. It resolves output to none for that session without mutating the configured format or other output settings. The immutable session retains dry_run=True, allowing later execution coordination to distinguish a dry run from another artifact-less session without rereading global configuration.

Presentation is independent of artifact selection:

  • -p / --preview always opens the completed artifact after rendering, for either renderer.
  • -l / --live-preview requests renderer-provided live display. OpenGL advertises support; Cairo rejects the request with a clear error.
  • Live preview with format=auto does not write a file. Passing a concrete video format displays and records simultaneously.
  • --show_in_file_browser remains a post-render action on the completed artifact.

Invalid combinations are rejected during session resolution rather than silently rewritten. This includes transparent MP4, section output with a non-video format, post-render preview without an artifact, live preview with dry_run, and live preview with final-state-only PNG output.

Implementation outline

  • Add OutputFormat and frozen, validated OutputSpec values.
  • Add frozen PresentationSpec and RenderSessionSpec values and a single resolve_render_session() entry point; the session retains dry_run separately from its effective output format.
  • Add declarative RendererCapabilities; Cairo declares live_preview=False and OpenGL declares live_preview=True.
  • Resolve the session once in Scene and pass it to renderer.init_scene(scene, session_spec).
  • Pass session_spec.output explicitly to SceneFileWriter.
  • Let Manager expose the captured session/output intent and perform post-render presentation from that snapshot.
  • Stop changing format, output_file, and related global output flags during finalization.
  • Record the completed artifact on SceneFileWriter.final_file_path; this may be a file or the image-sequence directory.
  • Preserve fast final-state rendering for -s and make --format=png use the same path.
  • Preserve the useful existing fallback where a video request for a scene with no play calls produces a still image rather than an empty movie.
  • Place PNG sequences in a dedicated scene directory, with paths such as media/images/<module>/<Scene>/0000.png.
  • Update notebooks, documentation directives, media opening, tests, and user documentation to consume the resolved result.

Breaking changes and migration

This RFC deliberately removes overlapping and already-deprecated interfaces rather than carrying synthetic compatibility state into the new model.

Removed CLI and config options

Removed interface Migration
--write_to_movie / [CLI] write_to_movie / config.write_to_movie Use format=auto or a concrete video format to write output; use format=none to disable it.
-g / --save_pngs, [CLI] save_pngs, and config.save_pngs Use --format=png-sequence / format=png-sequence.
-i / --save_as_gif, [CLI] save_as_gif, and config.save_as_gif Use --format=gif / format=gif.
[CLI] save_last_frame Use format=png. The -s CLI convenience and programmatic config.save_last_frame alias remain.
--force_window, [CLI] force_window, and config.force_window Use -l / --live-preview. Supply a concrete format as well if output should also be recorded.
Deprecated -f short form Use --show_in_file_browser.
config.movie_file_extension and config.resolve_movie_file_extension() Artifact and cached-segment extensions are derived from the resolved OutputSpec.

The format predicate helpers is_mp4_format, is_gif_format, is_png_format, is_webm_format, is_mov_format, and write_to_movie are also removed from manim.utils.file_ops. Callers should inspect the resolved OutputSpec instead.

Changed behavior

  • --format=png changes meaning. It previously wrote every rendered frame. It now uses the established fast final-state-only behavior of -s. Use --format=png-sequence for the old frame-sequence behavior.
  • PNG-sequence paths change. Frames now live in a scene-specific directory (<Scene>/0000.png) rather than being emitted beside one another with the scene name as a filename prefix.
  • OpenGL -p changes meaning. It no longer opens the live render window; it renders an artifact and opens that artifact afterward, matching Cairo. Use -l --renderer=opengl for live display.
  • OpenGL no longer silently disables automatic output. --renderer=opengl without live preview now follows normal format=auto behavior and writes MP4/MOV. --live-preview with format=auto remains display-only.
  • Transparent MP4 is an error. It is no longer silently changed to another container. Use format=auto, mov, or webm.
  • Non-video section output is an error. save_sections requires a video format.
  • Preview/reveal requests require an artifact. preview or show_in_file_browser with resolved format=none now fails early.
  • format=None or an empty format normalizes to auto. The stored configuration uses canonical format strings rather than “unset” as another output state.
  • Output and presentation settings are snapshots. Changes to those global config values after a Scene has been constructed no longer affect that scene. Callers using tempconfig must construct the scene inside the intended configuration context.
  • CLI/config precedence is corrected. An omitted CLI value no longer overwrites config-file values for options including renderer, transparent, and output_file; format is now actually loaded from config files. Users accidentally relying on the old overwrite/ignore behavior may see different output.
  • dry_run no longer mutates neighboring options. Code that inspected format, write_all, or still-output settings after setting dry_run will now see the original request preserved.
  • Rendering no longer replaces config.output_file with the completed path. config.output_file remains the requested name. Integrations that need the produced artifact must use scene.renderer.file_writer.final_file_path.
  • config.preview no longer aliases enable_gui. enable_gui is interpreted as a live-preview request during session resolution; preview exclusively means opening completed output.

Renderer and file-writer extension APIs

Custom renderers and direct file-writer users need updates:

  • Renderers must expose a capabilities: RendererCapabilities declaration.
  • renderer.init_scene(scene) becomes renderer.init_scene(scene, session_spec).
  • OpenGLRenderer.should_create_window() now receives the resolved session_spec.
  • SceneFileWriter(renderer, scene_name, ...) now requires an explicit output_spec argument. There is intentionally no global-config fallback.
  • Custom SceneFileWriter classes injected through a renderer must accept the new output_spec keyword.
  • open_media_file(file_writer) now requires explicit preview= and show_in_file_browser= keyword arguments and opens final_file_path instead of reconstructing paths from global config.

Scene.session_spec, Manager.session_spec, Manager.output_spec, and SceneFileWriter.output_spec are available to replace the old global reads.

Out of scope / follow-up work

This RFC does not yet:

  • implement a new Manager-owned exact no-raster evaluator for dry runs; this RFC preserves the execution request but does not add temporary renderer-specific evaluation paths;
  • move play, section, audio, segment, or artifact records fully into Manager;
  • decompose SceneFileWriter into segment encoding, audio timeline, and artifact assembly services;
  • add configurable PyAV codec/pixel-format/AVOption settings or include those settings in cache fingerprints;
  • redesign the media directory layout or remove custom_folders; or
  • change the current silent cached-video-segment strategy.

Those remain stacked follow-ups once the session intent and ownership boundary are agreed upon.

RFC questions

The main points on which feedback would be useful are:

  1. Are the canonical format values and the png / png-sequence distinction the right user model?
  2. Is making -p renderer-independent and reserving -l for live display the right split?
  3. Are the listed removals acceptable as one intentional breaking cleanup?
  4. Is Scene initialization the right point to freeze output and presentation intent?

(And just to be explicit about this: I've been working a bunch with GPT-5.6/Sol to put this together; everything in here has been reviewed at least coarsely by me though; critical parts and the general design shape were hand-crafted.)

@behackl behackl added refactor Refactor or redesign of existing code breaking changes This PR introduces breaking changes labels Aug 26, 2026
@behackl
behackl marked this pull request as ready for review August 27, 2026 16:21
@nikolajmunk

Copy link
Copy Markdown
Contributor

I'm hoping to take a closer look at this over the weekend, but here are some immediate thoughts. I'm not very familiar with the details of the current config-render-scene pipeline, so perhaps these are all obvious or irrelevant!

  • I really like the very explicit config model. IMO, Manim should do as little magic behind the scenes as possible, so this works well.
  • If I'm understanding you correctly, a render session is initially built from the provided config options. This session object is passed to the scene that is to be rendered, which then builds a session spec and uses that to provide output somehow. How does this work for rendering multiple scenes?
  • Something feels a bit off wrt the relationship between a scene and its config. Some things are obviously necessary for the scene to know what do to (camera size, frame rate, LaTeX templates, etc.). But does a scene really need to know things like output format or whether the file browser will be opened at the end? Those feel like things that should be owned by the renderer or a manager.

Exciting stuff so far 👍

@behackl

behackl commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Great questions!

  • As far as the design reaches right now, when rendering multiple scenes each scene would get a separate manager (and as an extension also a separate render session spec). When using the CLI (manim render ...), each call constructs a new manager with an independent spec etc.
  • You are absolutely right: the scene should not own output settings etc. at all! This is just a transitional design and makes sense because we more or less had it like that before -- but in a few PRs, handling these settings will indeed be taken away from the scene and moved to the manager.

@nikolajmunk

nikolajmunk commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Great questions!

  • As far as the design reaches right now, when rendering multiple scenes each scene would get a separate manager (and as an extension also a separate render > session spec). When using the CLI (manim render ...), each call constructs a new manager with an independent spec etc.
  • You are absolutely right: the scene should not own output settings etc. at all! This is just a transitional design and makes sense because we more or less had > it like that before -- but in a few PRs, handling these settings will indeed be taken away from the scene and moved to the manager.

Cool, I thought that might be the case! In my mind, a "session" would be the execution and rendering of all scenes provided by the user, but that's just nomenclature stuff. I'm also thinking about this from a non-CLI perspective (let's say I'm building an editor for Manim which builds its own manager or whatever), but none of this seems to directly preclude doing that, so I'm happy there.

Another off-the-cuff thought before I start looking at the code: Maybe I'm an extreme outlier and my workflow shouldn't weigh too heavily in these considerations, but I actually find it very useful to be able to invoke with tempconfig inside a scene. Here's an example of a dumb thing I'm currently doing to reuse scenes inside another scene:

class CombinedScene(Scene):
    def construct(self):
        square = Square()
        self.play(FadeIn(square))
        self.play(FadeOut(square))
        # this scene is very dense, so turn off caching
        with tempconfig(dict(disable_caching=True)):
            ReusableScene.construct(self)
        self.play(FadeOut(*self.mobjects))

Obviously I'll survive if I have to do something else! There's also this workaround which again isn't strictly necessary, but definitely a nice thing to have at your disposal.
Maybe there's a way to construct a temporary config on top of the current frozen one, so we don't mutate state but rather construct a data structure of stacked configs - sooort of similar to how setdefault works for mobjects and animations.

@nikolajmunk nikolajmunk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've only taken a cursory glance at the implementation itself so far, but here's a quick pass of documentation. Many of these were pretty LLM-y; in particular, I've noticed that LLMs love to write documentation that explains what was changed rather than what is now true about the code. For example it might write "this function accepts both string and integer input; integers are correctly cast to string and do not raise an error" rather than just "this function accepts string and int input". I've tried to make the wording clearer, more human-sounding and more useful to future users of Manim.

Hope these are of any use!

Comment thread docs/source/guides/deep_dive.rst Outdated
Comment thread docs/source/guides/deep_dive.rst Outdated
Comment thread docs/source/guides/deep_dive.rst Outdated
Comment on lines +308 to +312
scene captures the immutable session specification before renderer
initialization, while the renderer still owns its camera, clock, play count,
skip state, and file writer. The manager exposes the session and forwarding
views of renderer state. The scene and renderer therefore still have
substantial interplay that later refactors aim to remove.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what to do about this one, it's very hard to read so I don't understand the paragraph well enough to rephrase it 😅

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplified!

Comment thread docs/source/guides/deep_dive.rst Outdated
Comment thread docs/source/tutorials/output_and_config.rst Outdated
Comment thread docs/source/tutorials/output_and_config.rst Outdated
Comment on lines +343 to +344
Finally, by default Manim outputs ``.mp4`` files. To request GIF output instead,
use ``--format=gif``. GIF and final-frame PNG names include the installed Manim

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this paragraph necessary if we have the "Output formats" section above? Maybe this it could be moved there.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No - deleted.

Comment thread manim/_config/utils.py Outdated
def save_last_frame(self) -> bool:
"""Whether to save the last frame of the scene as an image file (-s)."""
return self._d["save_last_frame"]
"""Whether to use final-state-only PNG output (-s)."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal opinion: final-state-only PNG output" sounds ugly and it would be cool to find a better name. Even just "final-state PNG output" feels better!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've consistently renamed this to "last frame", which is much clearer IMO. Good call!

Comment thread docs/source/contributing/testing.rst Outdated
require rendering a video using the -l flag from a scene. Then we will test
(in this case, SquareToCircle), that lives in
``test_scene_rendering/simple_scene.py``. Change directories to ``tests/``,
For instance, a test that checks low-quality rendering first requires rendering

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, "...a test that checks low-quality rendering will first require rendering..." would work better here just to avoid the possible interpretation that the low-quality test is the first test of multiple to come.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed -- I've pushed an easier to read reworded version!

Comment thread docs/source/guides/deep_dive.rst Outdated
from presentation requests such as opening the completed artifact or displaying
a live preview. It also preserves whether dry-run execution was requested. A dry
run and an artifact-less render both have an effective output format of ``none``,
but the reason remains available without rereading mutable global configuration.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like "but a dry run is treated as if it were a normal render session with a provided output format." instead?

It doesn't feel useful to the reader to know what the system doesn't do.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed -- attempted to make the intent behind dry_run a bit clearer.

Co-authored-by: nikolajmunk <28557236+nikolajmunk@users.noreply.github.com>
@nikolajmunk

Copy link
Copy Markdown
Contributor

OK, as far as I can tell everything looks good on the code side. I have a few comments about config options:

  • Both the implementation and docs emphasize that if a video format is requested but the scene contains no play calls, then the renderer should produce a "useful still image", i.e., render a last-state PNG. Is this really the behavior we'd want?

    Part of the philosophy behind this PR is that a) configs are immutable once the scene begins and b) Manim should very rarely try and "fix" the user's config options, and this behavior is effectively the same as changing the config's output format to PNG. I think that's 100% fine if format = auto, but if the user has explicitly specified e.g. format = mp4, then that feels a little iffy. Here are two alternate options for when format = [video] but there are no play calls:

    1. Assume the user has done this on purpose and output the last (and only) frame of the scene to an mp4 (or whatever format the user requested). I don't know if this is technically possible or why someone would ever wanna do this, but it feels "honest" in the same way the new config design does.
    2. Keep the current behavior and output a PNG instead of a video, but also log a message or warning à la No animation frames were produced in {scene_name}. The output has been saved as an image instead.. Then the user is explicitly informed that we're going against the wishes stated in the config.
  • Currently, preview and show_in_file_browser are mutually exclusive. Is there any reason for this? I could easily see a user wanting both Finder and VLC to open when the render finishes. Order of opening could be determined either by the order the flags are provided, or we could pick one winner (always having the video player/image viewer open above the file system feels right to me).

  • Similarly, I'm not completely convinced that save_last_frame should set format = png. It seems perfectly reasonable for a user to want to render a video and a final-state PNG for, say, thumbnails. I could see it being an execution intent similar to dry-run: the scene behaves as it normally would based on the output format, but the final output is augmented by the execution intent. They also both have a similar canceling-out case: setting dry-run=True, output=none does the "same thing" twice, and save_last_frame=True, output=png does the same thing twice. If this isn't technically feasible, I think the current behavior is fine as well :)

As a side note, here's a possibly overengineered thing I'm wondering: would it perhaps make sense to encode the constraints of the config (e.g. not (transparent is True and format == 'mp4') and not (preview is True and format == 'none')) and store it in a SessionConstraints class or similar? This would provide a single source of truth for validation, and it might make for easy testing since we could exhaustively test that no invalid combinations of options are allowed. This is obviously easy to add later, so no worries if it's out of scope for this PR.

@behackl behackl left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with all direct suggestions, and attempted to improve wording for all other regions where you left comments as well. Thanks for the detailed look at this!

Comment thread docs/source/contributing/testing.rst Outdated
require rendering a video using the -l flag from a scene. Then we will test
(in this case, SquareToCircle), that lives in
``test_scene_rendering/simple_scene.py``. Change directories to ``tests/``,
For instance, a test that checks low-quality rendering first requires rendering

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed -- I've pushed an easier to read reworded version!

Comment thread docs/source/guides/deep_dive.rst Outdated
from presentation requests such as opening the completed artifact or displaying
a live preview. It also preserves whether dry-run execution was requested. A dry
run and an artifact-less render both have an effective output format of ``none``,
but the reason remains available without rereading mutable global configuration.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed -- attempted to make the intent behind dry_run a bit clearer.

Comment thread docs/source/guides/deep_dive.rst Outdated
Comment on lines +308 to +312
scene captures the immutable session specification before renderer
initialization, while the renderer still owns its camera, clock, play count,
skip state, and file writer. The manager exposes the session and forwarding
views of renderer state. The scene and renderer therefore still have
substantial interplay that later refactors aim to remove.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplified!

Comment on lines +343 to +344
Finally, by default Manim outputs ``.mp4`` files. To request GIF output instead,
use ``--format=gif``. GIF and final-frame PNG names include the installed Manim

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No - deleted.

Comment thread manim/_config/utils.py Outdated
def save_last_frame(self) -> bool:
"""Whether to save the last frame of the scene as an image file (-s)."""
return self._d["save_last_frame"]
"""Whether to use final-state-only PNG output (-s)."""

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've consistently renamed this to "last frame", which is much clearer IMO. Good call!

@behackl

behackl commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

OK, as far as I can tell everything looks good on the code side. I have a few comments about config options:

Thanks for the careful review, much appreciated!

  • Both the implementation and docs emphasize that if a video format is requested but the scene contains no play calls, then the renderer should produce a "useful still image", i.e., render a last-state PNG. Is this really the behavior we'd want?
    Part of the philosophy behind this PR is that a) configs are immutable once the scene begins and b) Manim should very rarely try and "fix" the user's config options, and this behavior is effectively the same as changing the config's output format to PNG. I think that's 100% fine if format = auto, but if the user has explicitly specified e.g. format = mp4, then that feels a little iffy. Here are two alternate options for when format = [video] but there are no play calls:

    1. Assume the user has done this on purpose and output the last (and only) frame of the scene to an mp4 (or whatever format the user requested). I don't know if this is technically possible or why someone would ever wanna do this, but it feels "honest" in the same way the new config design does.
    2. Keep the current behavior and output a PNG instead of a video, but also log a message or warning à la No animation frames were produced in {scene_name}. The output has been saved as an image instead.. Then the user is explicitly informed that we're going against the wishes stated in the config.

Ah, this is a great question! I tend to agree that any "smartness" in the handling of the output format should be restricted to when the output format is set to "auto". The current implementation more or less simply mimics the behavior on the main branch, but I wouldn't mind introducing another breaking change here.

There actually is an argument that could be made for the library always appending a 1-frame long animation at the end (which would resolve the issue reported many times that for a scene just containing a self.play(some_animation, run_time=1) call and nothing afterwards the last frame is not showing correctly, but it would mean that the video length would no longer be the sum of the run times in the scene, but the run time sum plus one frame instead -- but I feel like this is something that we should discuss a bit more broadly, and I don't want to hide something like this in the otherwise already massive PR.

I think what I'd like to do is implement a combination of your two suggestions: only let manim try to be smart about the output format while the value is "auto" (the default), raise an error when a video is requested in a scene without animations, and log a warning when "auto" resolves to an image instead of a video. Thoughts?

  • Currently, preview and show_in_file_browser are mutually exclusive. Is there any reason for this? I could easily see a user wanting both Finder and VLC to open when the render finishes. Order of opening could be determined either by the order the flags are provided, or we could pick one winner (always having the video player/image viewer open above the file system feels right to me).

Are you sure? This might just be explained incorrectly in the docs, the code just has two sequential if branches, not an if/elif.

  • Similarly, I'm not completely convinced that save_last_frame should set format = png. It seems perfectly reasonable for a user to want to render a video and a final-state PNG for, say, thumbnails. I could see it being an execution intent similar to dry-run: the scene behaves as it normally would based on the output format, but the final output is augmented by the execution intent. They also both have a similar canceling-out case: setting dry-run=True, output=none does the "same thing" twice, and save_last_frame=True, output=png does the same thing twice. If this isn't technically feasible, I think the current behavior is fine as well :)

It's an interesting suggestion, and sort of prompts the question whether it should be allowed to specify multiple output formats at once. I tend to agree, but would rather want to implement this as a fancy new feature later, separately from this refactor.

As a side note, here's a possibly overengineered thing I'm wondering: would it perhaps make sense to encode the constraints of the config (e.g. not (transparent is True and format == 'mp4') and not (preview is True and format == 'none')) and store it in a SessionConstraints class or similar? This would provide a single source of truth for validation, and it might make for easy testing since we could exhaustively test that no invalid combinations of options are allowed. This is obviously easy to add later, so no worries if it's out of scope for this PR.

Interesting. I am sort of satisfied with the restrictions being all spelled out in OutputSpec.__post_init__ -- but perhaps something for a later extension of the system when we, say, let users also pass custom encoder config options?

I'll push a commit to change the behavior overriding user intent with the output format when there are no animations, plus fix the wording regarding preview + open in file browser. Thanks again!

@nikolajmunk

Copy link
Copy Markdown
Contributor

I think what I'd like to do is implement a combination of your two suggestions: only let manim try to be smart about the output format while the value is "auto" (the default), raise an error when a video is requested in a scene without animations, and log a warning when "auto" resolves to an image instead of a video. Thoughts?

Yep, that's a very nice intermediary step!

Are you sure? This might just be explained incorrectly in the docs, the code just has two sequential if branches, not an if/elif.

Oop you're right! Both work at the same time on my machine. I had interpreted open_file as setting a variable for later, so I thought the latter in_browser argument would overwrite the former. Then it's just docs that need to be fixed, thanks!

sort of prompts the question whether it should be allowed to specify multiple output formats at once. I tend to agree, but would rather want to implement this as a fancy new feature later, separately from this refactor.

Agreed! I do like the idea of save_last_frame as execution intent along with live-preview and dry-run though. I think that class of behavior is interesting! Might be possible to expose hooks for user-written execution intents down the line.

Interesting. I am sort of satisfied with the restrictions being all spelled out in OutputSpec.post_init -- but perhaps something for a later extension of the system when we, say, let users also pass custom encoder config options?

I was envisioning __post_init__ using the constraint class to perform its validation, but again this is easily doable later.

@behackl

behackl commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

Pushed changes as discussed and fixed one test that failed as a consequence. I have also added a couple more cheap tests to make sure the behaviour is exactly what we want for now. (Need a lot of tests for the upcoming file writer decoupling and renderer migrations.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changes This PR introduces breaking changes refactor Refactor or redesign of existing code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants